package zedtoken

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"strconv"

	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"

	"github.com/authzed/spicedb/pkg/datastore"
	zedtoken "github.com/authzed/spicedb/pkg/proto/impl/v1"
)

// Public facing errors
const (
	errEncodeError = "error encoding zedtoken: %w"
	errDecodeError = "error decoding zedtoken: %w"
)

// ErrNilZedToken is returned as the base error when nil is provided as the
// zedtoken argument to Decode
var ErrNilZedToken = errors.New("zedtoken pointer was nil")

// legacyEmptyDatastoreID is the empty datastore ID for legacy tokens and cursors.
const legacyEmptyDatastoreID = ""

// TokenStatus is the status of a zedtoken.
type TokenStatus int

const (
	// StatusUnknown indicates that the status of the zedtoken is unknown.
	StatusUnknown TokenStatus = iota

	// StatusLegacyEmptyDatastoreID indicates that the zedtoken is a legacy token
	// with an empty datastore ID.
	StatusLegacyEmptyDatastoreID

	// StatusValid indicates that the zedtoken is valid.
	StatusValid

	// StatusMismatchedDatastoreID indicates that the zedtoken is valid, but the
	// datastore ID does not match the current datastore, indicating that the
	// token was generated by a different datastore instance.
	StatusMismatchedDatastoreID
)

// RevisionHolder is an interface for types that can provide a unique ID and revision information.
type RevisionHolder interface {
	// UniqueID returns the unique ID of the holder (typically: a datastore).
	UniqueID(context.Context) (string, error)

	// RevisionFromString converts a string representation of a revision to a Revision.
	RevisionFromString(string) (datastore.Revision, error)
}

const uniqueIDPrefixLength = 8

// MustNewFromRevisionForTesting generates an encoded zedtoken from an integral revision.
func MustNewFromRevisionForTesting(revision datastore.Revision) *v1.ZedToken {
	encoded, err := newFromRevision(revision, legacyEmptyDatastoreID)
	if err != nil {
		panic(err)
	}
	return encoded
}

// NewFromRevision generates an encoded zedtoken from an integral revision.
func NewFromRevision(ctx context.Context, revision datastore.Revision, ds RevisionHolder) (*v1.ZedToken, error) {
	datastoreUniqueID, err := ds.UniqueID(ctx)
	if err != nil {
		return nil, fmt.Errorf(errEncodeError, err)
	}

	return newFromRevision(revision, datastoreUniqueID)
}

// NewFromRevisionNoDatastoreID generates an encoded zedtoken from an integral revision without a datastore ID.
// This is only for use in legacy scenarios. Most callers should use NewFromRevision.
func NewFromRevisionNoDatastoreID(revision datastore.Revision) (*v1.ZedToken, error) {
	return newFromRevision(revision, legacyEmptyDatastoreID)
}

func newFromRevision(revision datastore.Revision, datastoreUniqueID string) (*v1.ZedToken, error) {
	datastoreUniqueIDPrefix := datastoreUniqueID
	if len(datastoreUniqueIDPrefix) > uniqueIDPrefixLength {
		datastoreUniqueIDPrefix = datastoreUniqueIDPrefix[0:uniqueIDPrefixLength]
	}

	toEncode := &zedtoken.DecodedZedToken{
		VersionOneof: &zedtoken.DecodedZedToken_V1{
			V1: &zedtoken.DecodedZedToken_V1ZedToken{
				Revision:                revision.String(),
				DatastoreUniqueIdPrefix: datastoreUniqueIDPrefix,
			},
		},
	}
	encoded, err := Encode(toEncode)
	if err != nil {
		return nil, fmt.Errorf(errEncodeError, err)
	}

	return encoded, nil
}

// Encode converts a decoded zedtoken to its opaque version.
func Encode(decoded *zedtoken.DecodedZedToken) (*v1.ZedToken, error) {
	marshalled, err := decoded.MarshalVT()
	if err != nil {
		return nil, fmt.Errorf(errEncodeError, err)
	}
	return &v1.ZedToken{
		Token: base64.StdEncoding.EncodeToString(marshalled),
	}, nil
}

// Decode converts an encoded zedtoken to its decoded version.
func Decode(encoded *v1.ZedToken) (*zedtoken.DecodedZedToken, error) {
	if encoded == nil {
		return nil, fmt.Errorf(errDecodeError, ErrNilZedToken)
	}

	decodedBytes, err := base64.StdEncoding.DecodeString(encoded.Token)
	if err != nil {
		return nil, fmt.Errorf(errDecodeError, err)
	}
	decoded := &zedtoken.DecodedZedToken{}
	if err := decoded.UnmarshalVT(decodedBytes); err != nil {
		return nil, fmt.Errorf(errDecodeError, err)
	}
	return decoded, nil
}

// DecodeRevision converts and extracts the revision from a zedtoken or legacy zookie.
func DecodeRevision(encoded *v1.ZedToken, ds RevisionHolder) (datastore.Revision, TokenStatus, error) {
	decoded, err := Decode(encoded)
	if err != nil {
		return datastore.NoRevision, StatusUnknown, err
	}

	switch ver := decoded.VersionOneof.(type) {
	case *zedtoken.DecodedZedToken_DeprecatedV1Zookie:
		revString := strconv.FormatUint(ver.DeprecatedV1Zookie.Revision, 10)
		parsed, err := ds.RevisionFromString(revString)
		if err != nil {
			return datastore.NoRevision, StatusUnknown, fmt.Errorf(errDecodeError, err)
		}
		return parsed, StatusLegacyEmptyDatastoreID, nil

	case *zedtoken.DecodedZedToken_V1:
		parsed, err := ds.RevisionFromString(ver.V1.Revision)
		if err != nil {
			return datastore.NoRevision, StatusUnknown, fmt.Errorf(errDecodeError, err)
		}

		if ver.V1.DatastoreUniqueIdPrefix == legacyEmptyDatastoreID {
			return parsed, StatusLegacyEmptyDatastoreID, nil
		}

		datastoreUniqueID, err := ds.UniqueID(context.Background())
		if err != nil {
			return datastore.NoRevision, StatusUnknown, fmt.Errorf(errDecodeError, err)
		}

		datastoreUniqueIDPrefix := datastoreUniqueID
		if len(datastoreUniqueIDPrefix) > uniqueIDPrefixLength {
			datastoreUniqueIDPrefix = datastoreUniqueIDPrefix[0:uniqueIDPrefixLength]
		}

		if ver.V1.DatastoreUniqueIdPrefix != datastoreUniqueIDPrefix {
			return parsed, StatusMismatchedDatastoreID, nil
		}

		return parsed, StatusValid, nil
	default:
		return datastore.NoRevision, StatusUnknown, fmt.Errorf(errDecodeError, fmt.Errorf("unknown zookie version: %T", decoded.VersionOneof))
	}
}
